package checker;

import java.util.ArrayList;

import util.AST.AST;
import util.AST.Comand;
import util.AST.Program;
import util.AST.AsgComd.AsgComd;
import util.AST.AsgComd.AsgComdExp;
import util.AST.AsgComd.AsgComdFuncCall;
import util.AST.Declarations.Declaration;
import util.AST.Declarations.FunctionDeclaration;
import util.AST.Declarations.ProcedureDeclaration;
import util.AST.Declarations.VarDeclaration;
import util.AST.Expression.BinaryExpression;
import util.AST.Expression.BooleanUnaryExpression;
import util.AST.Expression.Expression;
import util.AST.Expression.IdUnaryExpression;
import util.AST.Expression.NumberUnaryExpression;
import util.AST.Statement.AssignStatement;
import util.AST.Statement.ExitStatement;
import util.AST.Statement.FunctionCallStatement;
import util.AST.Statement.IfElseStatement;
import util.AST.Statement.PutLineStatement;
import util.AST.Statement.ReturnStatement;
import util.AST.Statement.Statement;
import util.AST.Statement.WhileStatement;
import util.AST.Terminal.ID;
import util.AST.Terminal.Number;
import util.AST.Terminal.Operator;
import util.AST.Terminal.Tipo;
import util.symbolsTable.IdentificationTable;

public class Checker implements Visitor {
	
	private IdentificationTable idTable;
	
	public Checker(){
		idTable = new IdentificationTable();
	}
	
	public void check(Program p) throws SemanticException{
		p.visit(this,null);
	}

	public Object visitAsgComdExp(AsgComdExp ac, Object o)
			throws SemanticException {
		 Tipo tp = (Tipo) ac.getExp().visit(this, o);
		 ac.getExp().setTipo(tp);
         
         return tp;
	}

	public Object visitAsgFuncCall(AsgComdFuncCall ac, Object o)
			throws SemanticException {
		FunctionCallStatement fc = ac.getCall();
        
        Tipo t = (Tipo) fc.visit(this, o);
        
        return t;
	}

	public Object visitAssignStatement(AssignStatement as, Object o)
			throws SemanticException {
		 VarDeclaration vd = (VarDeclaration) visitID(as.getId(), o);
         String type = vd.getTipo().getToken().getSpelling();
         AsgComd ac = as.getAssignComand();
         Tipo tac;
         
         if(ac instanceof AsgComdExp){
                 tac = (Tipo) visitAsgComdExp((AsgComdExp) ac, o);
         }else{
                 tac = (Tipo) visitAsgFuncCall((AsgComdFuncCall) ac, o);
         }
         
         if(!tac.getToken().getSpelling().equals(type)){
                 throw new SemanticException("DIFERENT TYPES");
         }
         
         return null;
	}

	public Object visitBinaryExpression(BinaryExpression be, Object o)
			throws SemanticException {
		   Tipo left;
           Operator operator = be.getOperator();
           Tipo right;
           Tipo tipoExp;
           
           left = (Tipo) be.getLeftExpression().visit(this, o);
           right= (Tipo) be.getRightExpression().visit(this, o);
           
           if(!left.getT().equals(right.getT())){
                   throw new SemanticException("DIFFERENT TYPES");
           }
           
           tipoExp = (Tipo) operator.visit(this, left.getToken().getSpelling());{
        	   return tipoExp;
           }	   
	}
	
	private Object verifyCondition(Expression e, Object o) throws SemanticException{
		Tipo t = null;
		
		if(e instanceof BinaryExpression){
			t = (Tipo) visitBinaryExpression((BinaryExpression)e, o);
		}else if(e instanceof IdUnaryExpression){
			t = (Tipo) visitIdUnaryExpression((IdUnaryExpression)e, o);
		}else if(e instanceof BooleanUnaryExpression){
			t = (Tipo) visitBooleanUnaryExpression((BooleanUnaryExpression)e, o);
		}else{
			throw new SemanticException("CONDITION HAS TO BE BOOLEAN");
		}
		
		if(t.getToken().getSpelling().equals("boolean"))
			throw new SemanticException("CONDITION HAS TO BE BOOLEAN");
		return t;
	}
	
	public Object visitExitStatement(ExitStatement es, Object o)
			throws SemanticException {
		 if(!(o instanceof WhileStatement)){
             throw new SemanticException("EXIT ONLY CAN BE USED IN A 'WHILE'");
     }
     
     return null;
	}

//	public Object visitExpression(Expression exp, Object o)
//			throws SemanticException {
//		Tipo t = (Tipo) verifyExp(exp, o);
//		return t;
//	}

	public Object visitFunctionCallStatement(FunctionCallStatement fcs, Object o)
			throws SemanticException {
		 
        ArrayList<ID> arguments = fcs.getVariables();
        FunctionDeclaration fd = (FunctionDeclaration) visitID(fcs.getName(), o);
        ArrayList<ID> parameters = fd.getArgs();
        Tipo returnType = fd.getType();
        
        if(parameters.size() != arguments.size()){
                throw new SemanticException("PARAMETERS FUNCIONT LIST INVALID");
        }
        
        ArrayList<ID> argumentsID = new ArrayList<ID>();
        for(ID id : arguments){
                argumentsID.add(id);
        }
        
        for(int i = 0; i < parameters.size(); i++){
                String currentTypeParameter = parameters.get(i).getToken().getSpelling();
                String currentTypeArgument = argumentsID.get(i).getToken().getSpelling();
                if(!currentTypeParameter.equals(currentTypeArgument)){
                        throw new SemanticException("PARAMETERS FUNCIONT LIST INVALID");
                }
        }
        
        return returnType;
	
	}

	public Object visitFunctionDeclaration(FunctionDeclaration fd, Object o)
			throws SemanticException {
		Boolean ret = false;

        String name = fd.getName().getToken().getSpelling();
        
        ArrayList<VarDeclaration> variables = fd.getNewVars();
        
        idTable.enter(name, fd);
        
        idTable.openScope();
        
        ArrayList<ID> parameters = fd.getArgs();
        
        if(parameters != null){
                for(ID var : parameters){
                        visitID(var, o);
                }
        }
        
        if(fd.getStatements().size() == 0){
        	throw new SemanticException("FUNCTION MUST HAVE A RETURN TYPE");
        }else{
        	int order = 1;
        	for(VarDeclaration vd : variables){
        		vd.setOrder(order);
				vd.setNode(fd);
				vd.visit(this, fd);
				order++;
        	}
        
	        ArrayList<Statement> statements = fd.getStatements();
	        for(Statement st : statements){
	        	Object v = st.visit(this, o);
	        	
	        	if(v instanceof Boolean){
	        		ret = (Boolean) v;
	        	}
	        }
	        if(!ret){
				throw new SemanticException("FUNCTION MUST HAVE A RETURN TYPE");
			}
	        
        
        }
        
        idTable.closeScope();
        
        return null;	
	}

	public Object visitID(ID id, Object o) throws SemanticException {
		  String name = id.getSt();
          Comand command = (Comand) idTable.retrieve(name);
          if(command == null){
                  throw new SemanticException("IDENTIFIER NOT FOUND");
          }
          return command;
	}

	public Object visitIdUnaryExpression(IdUnaryExpression iue, Object o)
			throws SemanticException {
		VarDeclaration vd = (VarDeclaration) iue.getId().visit(this, o);
		iue.getId().setNode(vd);
		Tipo t = vd.getTipo();
		iue.setTipo(t);
		
		return t;
	}

	public Object visitIfElseStatement(IfElseStatement ies, Object o)
			throws SemanticException {
		
		verifyCondition(ies.getCondition(), o);
		
		Object[] ast;
		Boolean hasReturn = false;
		
		if(o instanceof Object[]){ 
			
			int length = ((Object[]) o).length;
			ast = new Object[length+1];
			int i;
			for(i=0; i < length; i++){
				ast[i] = ((Object[]) o)[i];
			}
			
			ast[i] = ies;
		}else{
			ast = new Object[]{o,ies};
		}
		
		ArrayList<Statement> ifB = ies.getIfStatements();
		for(Statement stmt : ifB){
			Object visit = stmt.visit(this, ast);
			
			if(visit instanceof Boolean){
				hasReturn = (Boolean) visit;
			}			
		}
		
		ArrayList<Statement> elseBody = ies.getElseStatements();
		for(Statement st : elseBody){
			Object visit = st.visit(this, ast);
			
			if(visit instanceof Boolean){
				hasReturn = (Boolean) visit;
			}	
		}	
		
		
		return hasReturn;
	}

	public Object visitNumber(Number n, Object o) throws SemanticException {
		if(n.getToken().getSpelling().contains(".")){
			return new Tipo("float");
		}else{
			return new Tipo("int");
		}
	}

	public Object visitNumberUnaryExpression(NumberUnaryExpression nue, Object o)
			throws SemanticException {
		if(nue.getT().contains(".")){
			return new Tipo("float");
		}else{
			return new Tipo("int");
		}
	
	}

	public Object visitOperator(Operator op, Object o) throws SemanticException {
		String tipo = (String) o;
        String operator = op.getToken().getSpelling();
        
        if(operator.equals("+") || operator.equals("-") || operator.equals("*") || operator.equals("/")){
                if(!tipo.equals("int") && !tipo.equals("float")){
                        throw new SemanticException("Expressão incorreta");
                }
                
                return new Tipo(tipo);
                
        }else if(operator.equals("==") || operator.equals("!=")){
                if(!tipo.equals("int") && !tipo.equals("float") && !tipo.equals("boolean")){
                        throw new SemanticException("WRONG EXPRESSION");
                }
                
                return new Tipo("boolean");
        }else{
                if(!tipo.equals("int") && !tipo.equals("float")){
                        throw new SemanticException("WRONG EXPRESSION");
                }
                
                return new Tipo("boolean");
        }
	}

	public Object visitProcedureDeclaration(ProcedureDeclaration pd, Object o)
			throws SemanticException {

        String name = pd.getName().getToken().getSpelling();
        
        ArrayList<VarDeclaration> variables = pd.getVars();
        
        idTable.enter(name, pd);
        
        idTable.openScope();
        
        ArrayList<ID> parameters = pd.getArgs();
        
        if(parameters != null){
                for(ID var : parameters){
                        visitID(var, o);
                }
        }
        
        if(pd.getStatements().size() == 0){
        	throw new SemanticException("PROCEDURE MUST HAVE A RETURN");
        }else{
        	int order = 1;
        	for(VarDeclaration vd : variables){
        		vd.setOrder(order);
				vd.setNode(pd);
				vd.visit(this, pd);
				order++;
        	}
        
	        ArrayList<Statement> statements = pd.getStatements();
	        for(Statement st : statements){
	        	st.visit(this, o);
	        }
        
        }
        
        idTable.closeScope();
        
        return null;	
	}

	public Object visitProgram(Program p, Object o) throws SemanticException {
		 ArrayList<Comand> comands = p.getComand();
		 
         for(Comand c : comands){
                 c.visit(this, o);
         }
         
         ProcedureDeclaration pd = (ProcedureDeclaration) idTable.retrieve("main");
         
         if(pd == null)
         {
                 throw new SemanticException("MAIN PROCEDURE NOT FOUND");
         }
         
         return null;
	}

	public Object visitPutLineStatement(PutLineStatement pls, Object o)
			throws SemanticException {
		 Declaration dec = (Declaration) visitID(pls.getId(), o);
		 pls.getId().setNode(dec);
         
         if(dec instanceof FunctionDeclaration || dec instanceof ProcedureDeclaration){
                 throw new SemanticException("CANT PRINT A PROCEURE/FUNCTION");
         }
         
         return null;
	}

	public Object visitReturn(ReturnStatement rs, Object o)
			throws SemanticException {
		Tipo tp = (Tipo) rs.getReturnExpression().visit(this, o); 
		Tipo func;
		
		if(o instanceof Object[]){
			func = ((FunctionDeclaration) ((Object[]) o)[0]).getType();
		}else{
			func = ((FunctionDeclaration) o).getType();
		}
		
		
		if(!tp.getToken().getSpelling().equals(func.getToken().getSpelling())){
			throw new SemanticException("IT HAS TO RETURN THE SAME TYPE AS THE FUNCTION");
		}
		
		return true;
	}

	public Object visitTipo(Tipo t, Object o) throws SemanticException {
		return t;
	}


	public Object visitVarDeclaration(VarDeclaration vd, Object o) throws SemanticException {
		String tipo = vd.getTipo().getT();
        
        if(!(tipo.equals("float") || tipo.equals("int") || tipo.equals("boolean"))){
                throw new SemanticException("INVALID VARIABLE");
        }
        
        int ordem  = 1;
        for (ID id : vd.getVariable()) {
        	String name = id.getToken().getSpelling();
        	idTable.enter(name, vd);
        	id.setNode(vd);
        	id.visit(this, vd);
        	ordem++;
		}
        
        return null;
	}

	public Object visitWhileStatement(WhileStatement ws, Object o) throws SemanticException {
		 verifyCondition(ws.getCondition(), o);
         
		 Boolean ret = false;
			
			Object[] ast;
			
			if(o instanceof Object[]){
				
				int length = ((Object[]) o).length;
				ast = new Object[length+1];
				int i;
				for(i=0; i < length; i++){
					ast[i] = ((Object[]) o)[i];
				}
				
				ast[i] = ws;
			}else{
				ast = new Object[]{o,ws};
			}
			
			ArrayList<Statement> whileBody = ws.getStatements();
			
			for(Statement st : whileBody){
				Object visit = st.visit(this, ast); 
				
				if(visit instanceof Boolean){
					ret = (Boolean) visit;
				}	
			}
			
			return ret;
	}


	public Object visitBooleanUnaryExpression(BooleanUnaryExpression un, Object o) throws SemanticException {
		Tipo tipo = new Tipo("boolean");
		un.setTipo(tipo);
		return tipo;
	}

	public Object visitComand(Comand c, Object o) throws SemanticException {
		
		for (Declaration d : c.getFn()) {
			visitFunctionDeclaration((FunctionDeclaration) d, o);
		}
		for (Declaration d : c.getPd()) {
			visitProcedureDeclaration((ProcedureDeclaration) d, o);
		}
		return null;
		
	}
	public AST startChecker(AST program) throws SemanticException{
		program.visit(this, null); 
		
		return program;
	}

	public Object visitBoolean(util.AST.Terminal.Boolean b, Object o)
			throws SemanticException {
		return "boolean";
	}
		
}
